Classes
Python is a class-based inheritance language. In simple terms, classes are blueprints to create objects.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
return f"{self.name} says Woof!"
# Creating a dog object
my_dog = Dog("Buddy", 3)
print(my_dog.bark()) # Output: Buddy says Woof!
I. Core Class Components
1. __init__
method (Constructor)
The __init__
method is Python’s special initialization method (a constructor). When you create a new object from a class, Python automatically calls this method to set up the initial state of the object.
my_dog = Dog("Buddy", 3)
→ When Python executes this line, it:
- Creates a new empty object
- Calls
__init__
with the provided arguments - Returns the initialized object
2. self
and Instance Attributes
In Python, self
represents the specific instance of the class that’s being worked with. It's how the code knows which object's data to access or modify. Every instance method in a class automatically receives the instance (self
) as the first parameter.
def bark(self):
return f"{self.name} says Woof!"
Example:
class Library:
def __init__(self, name):
self.name = name
self.books = [] # Each library has its own book list
def add_book(self, book):
# self.books refers to THIS library's book list
self.books.append(book)
def book_count(self):
# Returns the count for THIS specific library
return len(self.books)
# Creating two different libraries
city_library = Library("City Library")
school_library = Library("School Library")
# Adding books to different libraries
city_library.add_book(gatsby) # Adds to city_library's books list
school_library.add_book(hobbit) # Adds to school_library's books list
# Each library has its own separate count
print(city_library.book_count()) # 1
print(school_library.book_count()) # 1
While it's convention to use the name
self
, Python cares about the position, not the name. This code would work the same way:
class Library:
def __init__(this_library, name): # "this_library" instead of "self"
this_library.name = name
this_library.books = []
3. Method Types
a. Instance Methods
Regular methods that access instance data
- First parameter is
self
- Can access/modify instance attributes
b. Class Methods
Methods that work with class variables.
@classmethod
def from_birth_year(cls, name, year):
return cls(name, 2024 - year)
c. Static Methods
Utility functions that don’t access instance/class data.
@staticmethod
def is_valid_name(name):
return bool(name.strip())
class Student:
# Class variable shared by all instances
school_name = "Python High"
student_count = 0
def __init__(self, name, age):
self._name = name
self._age = age
Student.student_count += 1
# Instance Method
# - Uses self to access instance data
# - Can modify instance state
def get_info(self):
return f"{self._name} is {self._age} years old"
# Class Method
# - Uses cls instead of self
# - Can access/modify class state
# - Can create class instances
@classmethod
def from_birth_year(cls, name, birth_year):
age = 2024 - birth_year
return cls(name, age)
@classmethod
def get_school_info(cls):
return f"{cls.school_name} has {cls.student_count} students"
# Static Method
# - Doesn't use self or cls
# - Utility function related to class purpose
# - Can't access instance or class state
@staticmethod
def is_valid_age(age):
return 0 <= age <= 120
@staticmethod
def calculate_grade_average(grades):
return sum(grades) / len(grades) if grades else 0
# Using different method types
# 1. Instance Method
student = Student("Alice", 20)
print(student.get_info()) # "Alice is 20 years old"
# 2. Class Methods
# Creating instance using alternate constructor
bob = Student.from_birth_year("Bob", 2000)
print(Student.get_school_info()) # "Python High has 2 students"
# 3. Static Methods
# Utility functions that don't need instance/class data
print(Student.is_valid_age(25)) # True
print(Student.calculate_grade_average([85, 90, 95])) # 90.0
4. Class Variables vs. Instance Variables
**Example: **class Library:
# Class variable - shared by ALL instances
total_books = 0
def __init__(self, name):
# Instance variables - unique to each instance
self._name = name
self._books = []
def add_book(self, book):
self._books.append(book)
# Updating the class variable
Library.total_books += 1
@property
def book_count(self):
return len(self._books)
@property
def books(self):
# Return a copy to prevent direct modification
return self._books.copy()
# Using the class
city_lib = Library("City")
school_lib = Library("School")
city_lib.add_book("1984")
school_lib.add_book("Hobbit")
print(city_lib.book_count) # 1
print(Library.total_books) # 2 (tracks books across ALL libraries)
II. Data Protection & Access Control
1. Encapsulation
Python uses naming conventions for different levels of attribute access. Python doesn’t enforce protection - it’s a convention.
1. Public Attributes (self.name
)
- Accessible from anywhere outside the class
- No special naming convention
- Used when you want the attribute to be part of the class’s public interface
2. Protected Attributes (self._name
)
- Single underscore prefix
- It is still technically accessible, but signals “internal use only”. This is a naming convention telling other developers “don’t access this directly”.
3. Private Attributes (self.__name
)
- Double underscore prefix
class Dog:
def __init__(self, name, age):
self.public = "Anyone can access" # Public attribute
self._protected = "Don't access directly" # Protected attribute
self.__private = "Hidden" # Private attribute (name mangled)
2. Properties (Getter and Setters)
Properties provide controlled access to class attributes, allowing validation and computed values.
a. Getters
Getter: A method decorated with @property
that returns a private attribute's value.
- Controls how private data is accessed
- Can compute values on-the-fly
b. Setters
Setter: A method decorated with @property_name.setter
that sets a private attribute's value.
- Controls how private data is modified
- Enables data validation
- Can transform input before storage
- Optional - omitting it makes the property read-only
class Dog:
def __init__(self, name, birth_year):
self._name = name
self._birth_year = birth_year
@property
def age(self):
# Age is computed from birth year
return 2025 - self._birth_year
@property
def human_age(self):
# Computed property based on another property
return self.age * 7
# Using computed properties
my_dog = Dog("Buddy", 2020)
print(my_dog.age) # 5 (computed from birth year)
print(my_dog.human_age) # 35 (computed from age)
- Data Validation
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if value < 0:
raise ValueError("Age cannot be negative")
self.__age = value
- Computed Values
@property
def full_name(self):
return f"{self.__first_name} {self.__last_name}"
III. Inheritance & Advanced Concepts
1. Basic Inheritance
Inheritance allows creating specialized versions of classes:
class Animal:
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
class Dog(Animal): # Dog inherits from Animal
def __init__(self, name, age, breed):
super().__init__(name, age) # Call parent class's __init__
self._breed = breed
@property
def name(self): # Override parent's property
return f"{self._name} the {self._breed}"
2. Multiple Inheritance
class Device:
def turn_on(self):
print("Device on")
class Radio:
def play_music(self):
print("Playing music")
class SmartRadio(Device, Radio): # Inherits from both classes
pass
# Can use methods from both parent classes
radio = SmartRadio()
radio.turn_on() # From Device
radio.play_music() # From Radio
3. Abstract Classes
Abstract classes are like blueprints for other classes. They define a common interface that all subclasses must implement.
from abc import ABC, abstractmethod
# Abstract base class for all animals
class Animal(ABC):
def __init__(self, name):
self.name = name
# Every animal must make a sound, but each does it differently
@abstractmethod
def make_sound(self):
pass
# A regular method all animals can use as-is
def introduce(self):
return f"I am {self.name}"
# A concrete class implementing Animal
class Cat(Animal):
def make_sound(self):
return "Meow!"
# Another concrete class
class Dog(Animal):
def make_sound(self):
return "Woof!"
# Using the classes
cat = Cat("Whiskers")
dog = Dog("Buddy")
print(cat.introduce()) # "I am Whiskers"
print(cat.make_sound()) # "Meow!"
print(dog.make_sound()) # "Woof!"
# This would raise an error - can't create abstract class
# my_animal = Animal("Generic") # Error!